Skip to content

fix: persist CREDENTIAL_SECRET in DATA_DIR to survive npm-global upgrades - #97

Closed
siglimumuni wants to merge 1 commit into
swarmclawai:mainfrom
siglimumuni:fix/credential-secret-persistence
Closed

fix: persist CREDENTIAL_SECRET in DATA_DIR to survive npm-global upgrades#97
siglimumuni wants to merge 1 commit into
swarmclawai:mainfrom
siglimumuni:fix/credential-secret-persistence

Conversation

@siglimumuni

Copy link
Copy Markdown
Contributor

Summary

`CREDENTIAL_SECRET` is persisted via `appendEnvKeyIfMissing` to `.env.local` inside `process.cwd()`. For npm-global installs, `cwd` is the per-version build directory (`~/.swarmclaw/builds/package-/.next/standalone/`). Each upgrade lands in a fresh build dir with a fresh empty `.env.local`; the auto-generate branch in `storage-auth.ts` fires (since the env var is unset in the new cwd); a brand-new 32-byte secret gets written.

Net effect: every npm-global upgrade silently regenerates `CREDENTIAL_SECRET`. Every credential encrypted under the prior secret becomes unreadable. The decrypt failure goes into `try/catch` blocks throughout the codebase and produces empty values — Slack connectors come up with `"No bot token configured"`, agent execute shells get empty env vars, every provider key in Settings → Providers needs to be re-entered.

How I hit this

Upgraded a running install from v1.9.31 → v1.9.32 (`npm install -g @swarmclawai/swarmclaw@1.9.32`). After `launchctl kickstart` brought the service back:

  • All 8 Slack connectors reported `lastError = "No bot token configured"`. Cycling them didn't help.
  • `gh` inside an agent's execute shell reported empty `$GITHUB_TOKEN` despite the credential being properly configured on the agent.
  • The credential records in storage are intact — `encryptedKey` field is unchanged. The decrypt just silently fails.

`diff`'d the two `.env.local`s:

```
v1.9.31: CREDENTIAL_SECRET=ac7e4c83…
v1.9.32: CREDENTIAL_SECRET=5529ec0f… ← regenerated during the upgrade
```

Restoring the old secret into v1.9.32's `.env.local` and restarting unblocked everything. But that workaround breaks on every future upgrade.

Fix

Store the secret in a dedicated file in `DATA_DIR` (`<DATA_DIR>/credential-secret`), which is stable across npm-global upgrades and already volume-mounted in Docker. Resolution precedence in `storage-auth.ts`:

  1. `process.env.CREDENTIAL_SECRET` (already set externally, e.g. by orchestrator)
  2. `<DATA_DIR>/credential-secret` — the new stable home
  3. `.env` files (legacy — values from `loadEnv()`)
  4. Generate + persist to `<DATA_DIR>/credential-secret`

Migration is automatic: on first launch after this fix lands, an existing secret found via `.env.local` gets copied into the dedicated file. Future upgrades read from the file directly. File permissions: `0o600`.

If both the env-loaded value AND the file are set and disagree, the file wins (with a `log.warn`) — that case typically means a stale `.env.local` was regenerated during install and would orphan working credentials if trusted.

Files

  • `src/lib/server/storage-auth.ts` — 70 lines

Test plan

  • Reviewed by inspection.
  • Applied locally as a workaround (manual secret restoration) to confirm credential decryption recovers.
  • Operator verification: after PR lands, upgrade across a minor version with this fix applied — confirm `<DATA_DIR>/credential-secret` exists with restrictive perms, secret survives, all credentials still decrypt.

Notes

`ACCESS_KEY` has the same per-version-cwd persistence problem (and gets regenerated on every upgrade too) but isn't as catastrophic — it just forces an auth re-login. Left for a follow-up if there's interest in symmetric treatment.

🤖 Generated with Claude Code

…ades

CREDENTIAL_SECRET was persisted to .env.local inside process.cwd(), which
for npm-global installs is the per-version build directory
(~/.swarmclaw/builds/package-<ver>/.next/standalone/). Each upgrade lands
in a fresh build dir with a fresh .env.local, the auto-generate branch
fires (since the env var is unset in the new cwd), and a brand-new secret
gets written.

Net effect: every npm-global upgrade silently regenerates
CREDENTIAL_SECRET. All previously-encrypted credentials — Slack bot
tokens, provider API keys, agent credential injections — silently fail
to decrypt (the failure goes into try/catch and produces empty values).
Slack connectors come up with "No bot token configured." Agent execute
shells get empty env vars. Recovery requires either restoring the old
secret manually or re-entering every credential.

Fix: store the secret in a dedicated file in DATA_DIR
(`<DATA_DIR>/credential-secret`), which is stable across upgrades and
volume-mounted in Docker. Resolution precedence is now:

  1. process.env.CREDENTIAL_SECRET (already-set, e.g. by orchestrator)
  2. <DATA_DIR>/credential-secret  ← the new stable home
  3. .env files (legacy — values from loadEnv())
  4. Generate + persist to <DATA_DIR>/credential-secret

Migration path is automatic: on first launch after upgrade, an existing
secret found via .env.local gets copied into the dedicated file. Future
upgrades read from the file directly. If both exist and disagree, the
file wins (with a log.warn) — that case usually means a stale .env.local
was generated by a previous regression and would orphan working
credentials if used.

File permissions: 0o600 on the secret file.

Concrete repro: install v1.9.31, configure agents with credentials,
`npm install -g @swarmclawai/swarmclaw@1.9.32`, restart service.
Every credential becomes unreadable.

Files:
  - src/lib/server/storage-auth.ts
@waydelyle

Copy link
Copy Markdown
Member

Cherry-picked and shipped in v1.9.33 with additional precedence and migration tests for DATA_DIR credential-secret handling. Thanks for the fix.

@waydelyle waydelyle closed this May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants